package executables;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.ListIterator;

import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;

import util.Metagenomes;
import util.SortedHits;
import data_structs.HashTable;
import data_structs.Hit;
import data_structs.SequenceWrapper;
import errors.IncorrectNucleotide;

public class SequenceMatcher {
	
	private static BufferedReader hashBuffer;
	private static BufferedReader searchBuffer;
	private static PrintWriter printer;
	
	private static HashTable<SequenceWrapper> ht;
	private static int kmer;
	private static int numInHashFile, numInQueryFile;
	private static int totalHashed, totalFound;
	
	private static ArrayList<String> toPrint;
	
	private static String comment1 = "SequenceWrapper and Hit objects use multiple integer variables for data fields.";
//	private static String comment2 = "SequenceWrapper and Hit objects use compressed integer variable for data fields";
	
	public static void main(String[] args) throws IOException, IncorrectNucleotide {	
		setNimbusLookAndFeel();
		
		File hashFile = new File(Metagenomes.WIN_51hits);
		File queryFile = new File(Metagenomes.WIN_microbe377small);
		
		
		
		String projName = makeFolder();
		totalHashed = 0;
		kmer = 15;
		
		//User input filename
		//If user input is blank, loop around
//		String inputname, fname;
//		for(;;) {
//			String defaultName = "results-"+getTime();
//			inputname = (String) JOptionPane.showInputDialog(null,"Name of results file","Save As...",
//					JOptionPane.QUESTION_MESSAGE,null,null,defaultName);
//			
//			if( inputname != null ) {
//				inputname = inputname.trim();
//				fname = (inputname.equals(defaultName)) ? "results/"+inputname+".txt" :
//					"results/"+inputname+"-"+getTime()+".txt";
//				break;
//			}
//		}
		
//		System.out.println("kmer "+ kmer + " filename " + fname + " timestampit " + timestampit);
		
		printer = new PrintWriter(new FileWriter(projName+"/results.txt"));
		printer.println(hashFile.getName()+"\t"+queryFile.getName());
		printer.println("Ref ID\tRef Start\tRef End\tQuery Start\tQueryEnd");
		
		//Timing variables
		long startHash, startSearch;
		double hTime, sTime;
		
		/********************************************************
		 * Hash Section
		 ********************************************************/
		//Read in hash file
		hashBuffer = new BufferedReader(new FileReader(hashFile));
		
		startHash = System.currentTimeMillis();
		int nameIdx = 1;
		
		//Read-hash-file loop
		for(;;) {
			numInHashFile = 0;
			//2778779 = a large random prime number
			ht = new HashTable<SequenceWrapper>(2778779);
			
			while( hashBuffer.ready() && numInHashFile <= 50000 ) {
				if( !insertToHashTable() ) throw new UnsupportedOperationException("Failed hash insertion");
				numInHashFile++;
			}
			//Serialize
//			System.out.println("Serializing");
//			ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(projName+"/hash"+nameIdx++));
//			out.writeObject(ht);
//			out.close();
			
			//Exit hash loop when reach EOF
			if( !hashBuffer.ready() ) break;
		}
		hashBuffer.close();
		hTime = (double) (System.currentTimeMillis()-startHash)/1000;
		
		/********************************************************
		 * Search Section
		 ********************************************************/
		System.out.println("Starting search...");
		numInQueryFile = totalFound = 0;
		startSearch = System.currentTimeMillis();
		
		//Read in query file
		searchBuffer = new BufferedReader(new FileReader(queryFile));
		
		//Read-query-file loop
		while( searchBuffer.ready() ) {
			if( !search() ) throw new UnsupportedOperationException("Failed search");
			
			numInQueryFile++;
			if( numInQueryFile % 1000 == 0 ) {
				System.out.println("Searched for "+numInQueryFile+" sequences.");
//				printer.close();
//				
//				cal = Calendar.getInstance();
//				sdf = new SimpleDateFormat("HH.mm.ss");
//				time = sdf.format(cal.getTime());
//				fname = "results/"+inputname+"/"+time+".txt";
//				printer = new PrintWriter(new FileWriter(fname));
			}
		}
		
		sTime = (double) (System.currentTimeMillis()-startSearch)/1000;
		double totalTime = (double) hTime+sTime;
		searchBuffer.close();
		
//		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("hash"));
//		out.writeObject(ht);
//		out.close();
		
		printer.close();
//		printer = new PrintWriter(new FileWriter(projName+"/time.txt",true));
		printer = new PrintWriter(new FileWriter("results/results", true));
//		printer.println("date\ttime\tref-file\tquery-file\thash-time\tsearch-time\ttotal-time\tcomments");
		printer.println(getDate()+"\t"+getTime()+"\t"+hashFile+"\t"+queryFile+"\t"+hTime+"\t"+sTime+"\t"+totalTime+"\t"+comment1);
//		printer.println("Hash time: "+hTime+" seconds");
//		printer.println("Search time: "+sTime+" seconds");
//		printer.println("Total time: "+totalTime+" seconds");
//		printer.println("Number of objects hashed: "+totalHashed);
//		printer.println("Number of "+kmer+"-mers found: "+totalFound);
//		printer.println("Number of keys in hash: "+ht.numKeys());
		printer.close();
		
		
		System.out.println("***Time Results***");
		System.out.println("Hash time: "+hTime+" seconds");
		System.out.println("Search time: "+sTime+" seconds");
		System.out.println("Total time: "+totalTime+" seconds");
		System.out.println("Number of objects hashed: "+totalHashed);
		System.out.println("Number of "+kmer+"-mers found: "+totalFound);
		System.out.println("Number of keys in hash: "+ht.numKeys());
		System.out.println(ht.hashStatus());
//		System.out.println("Memory left over: "+java.lang.Runtime.getRuntime().freeMemory());
	}//End main
	
	/***************************************************
	 * Utility methods
	 ***************************************************/
	
	/**
	 * Set the look and feel of javax.swing GUI to Nimbus look and feel
	 */
	public static void setNimbusLookAndFeel() {
		try {
			LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels();
			//Iterate through all installed look and feels to find Nimbus class
			for( LookAndFeelInfo laf: lafs )
				if( "Nimbus".equals(laf.getName()) )
					UIManager.setLookAndFeel(laf.getClassName());
		} catch(Exception e) {
			System.err.println("Error setting Nimbus LAF: " + e);
		}
	}
	
	/**
	 * Make a folder to hold results and hash objects
	 * @return Name of folder
	 */
	private static String makeFolder() {
		String projName = null;
		BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
		for(;;) {
			for(;;) {
				System.out.println("Name of folder: ");
				
				try {
					projName = input.readLine();
				} catch (IOException e) {
					e.printStackTrace();
				}
				
//				projName = (String)JOptionPane.showInputDialog(null,"Please name your folder (it must be unique)","Directory Name",
//						JOptionPane.QUESTION_MESSAGE,null,null,null);
				if( projName != null ) break;
			}
			
			File f = new File("results/"+projName);
			if( f.mkdir() ) break;
			System.out.println("Directory already exists. Continue anyway?(Y/N):");
			String in = null;
			try {
				in = input.readLine();
			} catch (IOException e) {
				e.printStackTrace();
			}
			if( in.equalsIgnoreCase("Y") ) break;
			
//			if( f.mkdir()) break;
//			JOptionPane.showMessageDialog(null, "Sorry, that folder already exists. Enter a different name.", "Directory Name Error",
//					JOptionPane.ERROR_MESSAGE);
		}
		return "results/"+projName;
	}
	
	/**
	 * Get the current system time
	 * @return Current system time in HH.mm.ss format
	 */
	private static String getTime() {
		Calendar cal = Calendar.getInstance();
		SimpleDateFormat sdf = new SimpleDateFormat("HH.mm.ss");
		return sdf.format(cal.getTime());
	}
	
	private static String getDate() {
		Calendar cal = Calendar.getInstance();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
		return sdf.format(cal.getTime());
	}
	
	/**
	 * Read next sequence from the reference metagenome file and insert into hash table
	 * @return 
	 * @throws IOException
	 */
	private static boolean insertToHashTable() throws IOException {
		//Get current ID and sequence
		String id = (hashBuffer.readLine()).substring(1);
		String sequence = hashBuffer.readLine();
		
		System.out.println("Hashing id#"+id);
		
		int seqIdx = 0; //Use for position of sequence
		int endLoop = sequence.length()-kmer; //Stopping condition
		while( seqIdx <= endLoop ) {
			try {
				String seq = sequence.substring(seqIdx, seqIdx+kmer);
				String compSeq = reversal(getComplement(seq)); //Complement sequence
				
				//Objects to add to hash table
				SequenceWrapper sw = new SequenceWrapper(id, seq, seqIdx, false);
				SequenceWrapper compSW = new SequenceWrapper(id, compSeq, seqIdx, true);
				ht.add(seq, sw);
				ht.add(compSeq, compSW);
				seqIdx++; //Increment position of sequence
				totalHashed += 2;
			} catch(IncorrectNucleotide e) {
				e.printStackTrace();
				return false;
			}
		} //End while loop through sequence
		return true;
	}//End insertToHashTable
	
	/**
	 * Use the StringBuffer class reversal method to reverse a sequence
	 * @param seq: The String sequence to reverse
	 * @return The reverse form of the input sequence
	 */
	private static String reversal(String seq) {
		return seq = new StringBuffer(seq).reverse().toString();
	}
	
	/**
	 * Get the complement sequence of a specific sequence
	 * @param seq: The String sequence to get the complement sequence of
	 * @return The complement sequence form of the input sequence
	 * @throws IncorrectNucleotide: When an incorrect nucleotide is found
	 */
	private static String getComplement(String seq) throws IncorrectNucleotide {
		String ret = "";
		for( int i = 0; i < seq.length(); i++ ) {
			char curr = seq.charAt(i);
			switch(curr) {
				case 'A': ret += "T"; break;
				case 'T': ret += "A"; break;
				case 'C': ret += "G"; break;
				case 'G': ret += "C"; break;
				case 'N': ret += "N"; break;
				default: throw new IncorrectNucleotide("Nucleotide '" + curr + "' is not valid.");
			}
		}
		return ret;
	}//End getComplement
	
	/**
	 * Search for next sequence and record matches in a text file
	 * @return True if the search had no errors
	 * @throws IOException
	 */
	private static boolean search() throws IOException {
		String txt = searchBuffer.readLine();
		String queryId = txt.substring(1);
		String sequence = searchBuffer.readLine();
		
		int seqIdx = 0; //Use for position of sequence
		int endLoop = sequence.length()-kmer; //Stopping condition
		ArrayList<Hit> hits = new ArrayList<Hit>(); //Use to store hits
		while( seqIdx <= endLoop ) {
			try {
				String seqToFind = sequence.substring(seqIdx, seqIdx+kmer);
				
				SequenceWrapper sw = new SequenceWrapper(queryId,seqToFind,seqIdx,false);
				ArrayList<SequenceWrapper> matches = ht.contains(seqToFind, sw);
				if( matches != null && matches.size() != 0 ) {
					totalFound++;
					ListIterator<SequenceWrapper> matchIter = matches.listIterator();
					while( matchIter.hasNext() ) {
						SequenceWrapper sw_tmp = matchIter.next();
						if( !sw.isComplement() )
//							hits.add(new Hit(sw_tmp.getID(), sw_tmp.getPosition()-seqIdx, sw_tmp.getPosition(), seqIdx));
							hits.add(new Hit(sw_tmp.getID(), sw_tmp.getPosition(), seqIdx));
					}
				}
				seqIdx++;
			} catch (IncorrectNucleotide e) {
				System.err.println("id: "+queryId+", sequence:"+sequence+", position:"+seqIdx);
				e.printStackTrace();
				return false;
			}
			
		}//End while loop through sequence

		//Sort hits and find any significant runs of matches for current query sequence
		if( !hits.isEmpty() ) {
			getResults(hits, queryId);
			
			/*
			 * Now located in getResults() method
			SortedHits sortedHits = new SortedHits(hits);
			boolean printedQuery = false;
			for( Integer currId : sortedHits.getIds() ) {
				toPrint = new ArrayList<String>();
				
				for( Integer currShift : sortedHits.getShifts(currId) ) {
					findAlignments(sortedHits.getHits(currId, currShift), currId, currShift);
				}
				
				if( !toPrint.isEmpty() ) {
					for( String s : toPrint ) {
						String[] info = s.split(",");
						if( !printedQuery ) {
							printer.println(">"+queryId);
							printedQuery = true;
						}
						printer.println(currId+"\t"+info[0]+"\t"+info[1]+"\t"+info[2]+"\t"+info[3]);
					}
				}
			}*/
			
			/*
			HashMap<Integer,HashMap<Integer,ArrayList<Hit>>> sortedHits = sortHits(hits);
			Set<Integer> idKeys = sortedHits.keySet();			
			for( Integer ids : idKeys ) {
				HashMap<Integer,ArrayList<Hit>> shifts = sortedHits.get(ids);
				Set<Integer> shiftKeys = shifts.keySet();
				prevAlignStart = prevAlignEnd = -1;
				alignToPrint = false;
				toPrint = new ArrayList<String>();
				//Set low and high positions to fail on default
				//This way, any value will override values during alignment compare
				for( Integer shiftnums : shiftKeys ) {
					findAlignments(shifts.get(shiftnums), ids, shiftnums);
				}
				//Print out last alignment
	//			if( alignToPrint )
	//				printer.println("ID: "+ids+", Begin: "+finalStart+", End: "+finalEnd+", Query Start:"+finalQStart+", Query End: "+finalQEnd);
				if( !toPrint.isEmpty() ) {
					for( String s : toPrint ) {
						String[] info = s.split(",");
						if( !printedQuery ) {
							printer.println(">"+id);
							printedQuery = true;
						}
						printer.println(ids+"\t"+info[0]+"\t"+info[1]+"\t"+info[2]+"\t"+info[3]);
					}
				}
			}*/
		}
		
		return true;
	}//End searchForSeq
	
	/*
	 * Old Methods. New SortedHits class replaced.
	 * 
	 * private static HashMap<Integer,HashMap<Integer,ArrayList<Hit>>> sortHits(ArrayList<Hit> hits) {
		HashMap<Integer,HashMap<Integer,ArrayList<Hit>>> ids = new HashMap<Integer,HashMap<Integer,ArrayList<Hit>>>();
		HashMap<Integer,ArrayList<Hit>> shifts;
		ArrayList<Hit> offsets;
		//Cycle through all hits in array
		//Start at end of array for easy deletion after copying into sorted
		for( int i = hits.size()-1; i >= 0; i-- ) {
			Hit hit = hits.get(i);
			//Find correct array to store the hit
			if( (shifts = ids.get(hit.id)) == null ) {
				ids.put(hit.id, new HashMap<Integer,ArrayList<Hit>>());
				shifts = ids.get(hit.id);
			}
			
			if( (offsets = shifts.get(hit.shift)) == null ) {
				shifts.put(hit.shift, new ArrayList<Hit>());
				offsets = shifts.get(hit.shift);
			}
			//When correct array is found, perform insertion sort to add
			offsets = InsertionSort(offsets, hit);
			hits.remove(i);
		}
		return ids;
	}
	
	//Insert hit into correct position
	private static ArrayList<Hit> InsertionSort(ArrayList<Hit> offsets, Hit hit) {
		for( int i = 0; i < offsets.size(); i++ ) {
			if( hit.offset < offsets.get(i).offset ) {
				offsets.add(i,hit);
				return offsets;
			}
		}
		//End of array list reached, add to end
		offsets.add(hit);
		return offsets;
	}*/
	
	/**
	 * Print out matches found from searched sequence
	 * @param hits: Unsorted ArrayList of hits
	 * @param queryId: Sequence ID of current query sequence
	 */
	private static void getResults(ArrayList<Hit> hits, String queryId) {
		SortedHits sortedHits = new SortedHits(hits);
		boolean printedQuery = false;
		for( Integer currId : sortedHits.getIds() ) {
			toPrint = new ArrayList<String>();
			
			for( Integer currShift : sortedHits.getShifts(currId) ) {
				findAlignments(sortedHits.getHits(currId, currShift));
			}
			
			if( !toPrint.isEmpty() ) {
				for( String s : toPrint ) {
					String[] info = s.split(",");
					if( !printedQuery ) {
						printer.println(">"+queryId);
						printedQuery = true;
					}
					printer.println(currId+"\t"+info[0]+"\t"+info[1]+"\t"+info[2]+"\t"+info[3]);
				}
			}
		}
	}
	
	/**
	 * Find matches that span 2*kmer length nucleotides
	 * @param hits ArrayList of hits to search through
	 */
	private static void findAlignments(ArrayList<Hit> hits) {
		int startOfAlignment, prevOffset, nextOffset, end, queryStart, queryEnd;
		prevOffset = startOfAlignment = hits.get(0).getOffset()	;
		queryStart = hits.get(0).getQuery();
		//Cycle through the list of hits
		for( int idx = 1; idx < hits.size(); idx++ ) {
			nextOffset = hits.get(idx).getOffset();
			
			//Check if the next hit is too far away from the previous hit
			//This will end the run of alignment sequence
			if( nextOffset > prevOffset+1 ) {
				end = prevOffset+kmer-1;
				queryEnd = queryStart + (end-startOfAlignment);
				//Check there was a significant run of hits
				//Significant run = 2*kmer base length hit
				if( startOfAlignment+(2*kmer) <= prevOffset+kmer-1 ) {
//					printer.println("ID: "+id+" Begin: "+startOfAlignment+" End: "+end+" Query Start: "+queryStart+" Shift: "+shift);
//					alignToPrint = true;
//					checkAlignments(id, startOfAlignment, end, queryStart, queryEnd, shift);
					checkAlignments2(startOfAlignment, end, queryStart, queryEnd);
				}
					
				startOfAlignment = nextOffset;
				queryStart = hits.get(idx).getQuery();
			}
			
			//Check if the end of the array of hits has been reached...
			//and if there was a significant run of hits
			else if( idx == hits.size()-1 && startOfAlignment+(2*kmer) <= nextOffset+kmer-1 ) {
//				printer.println("ID: "+id+" Begin: "+startOfAlignment+" End: "+(nextOffset+kmer-1)+" Query Start: "+queryStart+" Shift: "+hits.get(0).shift);
				end = nextOffset+kmer-1;
				queryEnd = queryStart + (end-startOfAlignment);
//				alignToPrint = true;
//				checkAlignments(id, startOfAlignment, end, queryStart, queryEnd, shift);
				checkAlignments2(startOfAlignment, end, queryStart, queryEnd);
			}
			prevOffset = nextOffset;
		}
	}
	
	/**
	 * Check if there is an alignment that is similar to the new alignment. If so, combine them. Otherwise, store new alignment.
	 * @param currStart The start of the reference hit
	 * @param currEnd The end of the reference hit
	 * @param currQStart The start of the query sequence
	 * @param currQEnd The end of the query sequence
	 */
	private static void checkAlignments2(int currStart, int currEnd, int currQStart, int currQEnd) {
		//Check if there's a previous alignment to compare against
		if( toPrint.isEmpty() ) 
			toPrint.add(currStart+","+currEnd+","+currQStart+","+currQEnd);
		
		else {
			//Iterate through previous alignments to check
			for( int i = 0; i < toPrint.size(); i++ ) {
				//Get the previous low and high
				//Format: "alignStart,alignEnd,queryStart,queryEnd"
				String[] info = toPrint.get(i).split(",");
				int prevStart = Integer.parseInt(info[0]);
				int prevEnd = Integer.parseInt(info[1]);
				int prevQStart = Integer.parseInt(info[2]);
				int prevQEnd = Integer.parseInt(info[3]);
				
				//Check if alignments are similar
				if( currStart <= prevEnd && currEnd >= prevStart ) {
					//Replace new lows and highs and break
					int newStart = (prevStart < currStart) ? prevStart : currStart;
					int newEnd = (prevEnd > currEnd) ? prevEnd : currEnd;
					int newQStart = (prevQStart < currQStart) ? prevQStart : currQStart;
					int newQEnd = (prevQEnd > currQEnd) ? prevQEnd : currQEnd;
					toPrint.set(i, newStart+","+newEnd+","+newQStart+","+newQEnd);
					return;
				}
			}
			//Reached only if no similar alignments were found
			toPrint.add(currStart+","+currEnd+","+currQStart+","+currQEnd);
		}
	}
	/*
	 * Old Method
	 * 
	private static void checkAlignments(int id, int currStart, int currEnd, int currQStart, int currQEnd, int currShift) {
		//Check if there's a previous alignment to compare against
		if( prevAlignStart != -1 ) {
			int startDiff = currStart - prevAlignStart;
			int shiftDiff = currShift - prevAlignShift;
			int queryDiff = currQStart - prevQStart;
			//Check if alignments are not similar
			if( currStart > prevAlignEnd || prevAlignStart > currEnd || startDiff != (shiftDiff+queryDiff) ) {
				//Print out previous alignment; Set previous variables as current
				printer.println("ID: "+id+", Begin: "+finalStart+", End: "+finalEnd+", Query Start:"+finalQStart+", Query End: "+finalQEnd);
				prevAlignStart = finalStart = currStart;
				prevAlignEnd = finalEnd = currEnd;
				prevAlignShift = currShift;
				prevQStart = finalQStart = currQStart;
				prevQEnd = finalQEnd = currQEnd;
			}
			//There is a similar alignment
			else {
				//Check where the new start/end is
				finalStart = (finalStart < currStart) ? finalStart : currStart;
				finalEnd = (finalEnd > currEnd) ? finalEnd : currEnd;
				finalQStart = (finalQStart < currQStart) ? finalQStart : currQStart;
				finalQEnd = (finalQEnd > currQEnd) ? finalQEnd : currQEnd;
			}
		}
		//There is no previous alignment
		//Set variables
		else {
			prevAlignStart = finalStart = currStart;
			prevAlignEnd = finalEnd = currEnd;
			prevAlignShift = currShift;
			prevQStart = finalQStart = currQStart;
			prevQEnd = finalQEnd = currQEnd;
		}
	}*/
	
	
	
	
	
	
	
	
	
}//End class